﻿namespace NTLib.Core.Timers
{
    using Log;
    using Manipulators;
    using Memory;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using Threading;

    /// <summary>
    /// PCL timer implementation
    /// </summary>
    /// <seealso cref="Disposable" />
    public sealed class Timer : Disposable
    {
        #region Fields

        private readonly object _state;
        private readonly ReaderWriterLockSlim _lock;
        private TimeSpan _interval;
        private CancellationTokenSource _loopToken;
        private Action<object> _internalElasped;
        private bool _isRunning;
        private Task _runningTask;

        #endregion

        #region Ctor

        /// <summary>
        /// Initializes a new instance of the <see cref="Timer"/> class.
        /// </summary>
        public Timer(TimeSpan interval = default(TimeSpan), object state = null)
            : base()
        {
            this._state = state;
            this._interval = interval;
            this._lock = new ReaderWriterLockSlim();
        }

        #endregion

        #region Event

        /// <summary>
        /// Occurs when timer tick.
        /// </summary>
        public event Action<object> Elapsed
        {
            add
            {
                using (this._lock.LockWrite())
                {
                    _internalElasped += value;
                }
            }
            remove
            {
                using (this._lock.LockWrite())
                {
                    _internalElasped -= value;
                }
            }
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the timer tick interval.
        /// Any changed will reset the timer.
        /// </summary>
        /// <value>
        /// The interval.
        /// </value>
        public TimeSpan Interval
        {
            get
            {
                using (this._lock.LockRead())
                    return this._interval;
            }
            set
            {
                bool needReset = false;
                using (this._lock.LockWrite())
                {
                    needReset = this._interval != value && this._isRunning;
                    this._interval = value;
                }
                if (needReset)
                {
                    Stop();
                    Start();
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is running.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance is running; otherwise, <c>false</c>.
        /// </value>
        public bool IsRunning
        {
            get
            {
                using (this._lock.LockRead())
                    return this._isRunning;
            }
            private set
            {
                using (this._lock.LockWrite())
                    this._isRunning = value;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Starts the specified timer with a cancellation source to stop this one.
        /// </summary>
        /// <param name="token">The token.</param>
        public void Start(CancellationToken token = default(CancellationToken))
        {
            using (this._lock.LockWrite(token))
            {
                if (this._isRunning)
                    return;
                this._isRunning = true;
            }

            this._loopToken = CancellationTokenSource.CreateLinkedTokenSource(token);

            _runningTask = Task.Delay((int)this._interval.TotalMilliseconds, this._loopToken.Token)
                .ContinueWith(async (t, s) =>
            {
                CancellationTokenSource loopToken = s as CancellationTokenSource;
                while (true)
                {
                    using (this._lock.LockWrite())
                    {
                        if (loopToken.Token.IsCancellationRequested)
                            return;

                        _runningTask = Task.Delay((int)this._interval.TotalMilliseconds, loopToken.Token);
                    }

                    var running = Task.Run(() =>
                    {
                        try
                        {
                            if (this._internalElasped != null)
                                this._internalElasped(this._state);
                        }
                        catch (Exception ex)
                        {
                            NTLog.Instance.RecordError(ex, nameof(Timer));
                        }
                    }, loopToken.Token).ConfigureAwait(false);

                    try
                    {
                        await _runningTask;
                    }
                    catch (TaskCanceledException)
                    {
                        return;
                    }
                }

            }, this._loopToken, CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion,
            TaskScheduler.Default);
        }

        /// <summary>
        /// Stops this timer.
        /// </summary>
        public void Stop()
        {
            using (this._lock.LockWrite())
            {
                if (!this._isRunning)
                    return;
                this._isRunning = false;

                this._loopToken.Cancel();
                this._loopToken = null;
            }

            try
            {
                _runningTask.Wait();
            }
            catch (AggregateException)
            {

            }
        }

        /// <summary>
        /// Disposes the every time.
        /// </summary>
        protected override void DisposeEveryTime()
        {
            Stop();
            base.DisposeEveryTime();
        }

        #endregion
    }
}
